using System;
using System.Collections.Generic;
using System.Diagnostics;

namespace ArtSpec
{
    internal class SpecifyContext : ISpecifyContext, ISpec
    {
        private readonly SpecifyContext parent;

        public string Description { get; private set; }
        public Exception Error
        {
            get
            {
                Debug.Assert(verified, "Specification should be verified first");
                return error;
            }
        }

        public event Action<SpecifyContext> OnContextChange;
        public event Action<ISpec> OnResult;

        public SpecifyContext(SpecifyContext parent, string description)
        {
            this.parent = parent;
            if (parent == null) specified = true;
            Description = description;
        }

        public void Do(Action action)
        {
            Debug.Assert(!specified, "Context should be specified only once");
            OnContextChange(this);
            try
            {
                action();
            }
            catch (Exception e)
            {
                specs.Clear();
                contexts.Clear();
                error = new SpecException("Context specification error", e);
            }
            OnContextChange(parent);
            specified = true;
        }

        private Action beforeAll;
        public Action BeforeAll
        {
            get { return beforeAll; }
            set
            {
                if (beforeAll != null)
                    throw new SpecException("BeforeAll can be declared only once in a context");
                beforeAll = value;
            }
        }

        private Action beforeEach;
        public Action BeforeEach
        {
            get { return beforeEach; }
            set
            {
                if (beforeEach != null)
                    throw new SpecException("BeforeEach can be declared only once in a context");
                beforeEach = value;
            }
        }

        private Action afterEach;
        public Action AfterEach
        {
            get { return afterEach; }
            set
            {
                if (afterEach != null)
                    throw new SpecException("AfterEach can be declared only once in a context");
                afterEach = value;
            }
        }

        private Action afterAll;
        public Action AfterAll
        {
            get { return afterAll; }
            set
            {
                if (afterAll != null)
                    throw new SpecException("AfterAll can be declared only once in a context");
                afterAll = value;
            }
        }

        private readonly List<ISpecifyContext> contexts = new List<ISpecifyContext>();
        private readonly List<ISpecifyIt> specs = new List<ISpecifyIt>();
        private bool specified;
        private Exception error;
        private bool verified;

        public event Action OnBeforeEach;
        public event Action OnAfterEach;

        public void Add(ISpecifyContext context)
        {
            context.BeforeEach = HandleBeforeEach;
            context.AfterEach = HandleAfterEach;
            contexts.Add(context);
        }

        private void HandleAfterEach()
        {
            Fire.Event(OnAfterEach);
            Fire.Event(AfterEach);
        }

        private void HandleBeforeEach()
        {
            Fire.Event(BeforeEach);
            Fire.Event(OnBeforeEach);
        }

        public void Add(ISpecifyIt spec)
        {
            spec.OnBefore += HandleBeforeEach;
            spec.OnAfter += HandleAfterEach;
            spec.OnResult += sender => Fire.Event(OnResult, sender);
            specs.Add(spec);
        }

        public void Verify()
        {
            verified = true;
            if (!specified)
            {
                error = new SpecNotDefinedException();
                Fire.Event(OnResult, this);
                return;
            }

            try
            {
                Fire.Event(BeforeAll);
                specs.ForEach(spec => spec.Verify());
                contexts.ForEach(context => context.Verify());
                Fire.Event(AfterAll);
            }
            catch(Exception e)
            {
                error = e;
            }
            Fire.Event(OnResult, this);
        }
    }
}